package fr.n7.khome.core;

import java.io.Serializable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.management.InvalidAttributeValueException;

import org.apache.log4j.Logger;

import fr.n7.khome.util.FuzzyFormatException;
import fr.n7.khome.util.InvalidFuzzyValueException;

/**
 * This class implements a fuzzy number, composed of 4 bounds.
 * 
 *       a____b
 *       /     \
 *      /       \
 *   a-alpha  b+beta
 *    
 * Moreover, there are some methods in order to create fuzzy numbers
 * from a human readable string, and represent fuzzy numbers as strings.
 * 
 * In this class, a pattern matching  can be done from 2 fuzzy(a pattern and a data...)
 * to get the likelihood and the necessity. 
 * 
 * @author bbaccot && clej37
 *
 */
public class Fuzzy implements Serializable{

	private static final long serialVersionUID = -2487567913050831168L;
	
	private static Logger logger = Logger.getLogger(Fuzzy.class);

	/**
	 * It represents the degree of incertitude for a tilde, in the human
	 * readable representation.
	 * INCREMENT = 20 <=> ~ = 20%
	 */
	public static final float INCREMENT = 20;

	/**
	 * The 4 bounds for a fuzzy number
	 */
	private float a, b, alpha, beta;
	
	/**
	 * Create a fuzzy number using the internal representation (4 bounds)
	 *       a____b
	 *       /       \
	 *      /         \
	 *   a-alpha  b+beta
	 * 
	 * @param a - a as shown on the figure
	 * @param b - b as shown on the figure
	 * @param alpha - alpha as shown on the figure
	 * @param beta - beta as shown on the figure
	 * @throws InvalidFuzzyValueException 
	 */
	public Fuzzy(float a, float b, float alpha, float beta) throws InvalidFuzzyValueException {
		boolean argsOks = true;
		argsOks &= (Float.compare(a, Float.NaN)!=0);
		argsOks &= (Float.compare(b, Float.NaN)!=0);
		argsOks &= (Float.compare(alpha, Float.NaN)!=0);
		argsOks &= (Float.compare(beta, Float.NaN)!=0);
		argsOks &= (Float.compare(a, b) <= 0);
		argsOks &= (Float.compare(alpha, 0) >= 0);
		argsOks &= (Float.compare(beta, 0) >= 0);
		if(!argsOks){
			throw new InvalidFuzzyValueException("("+a+", "+b+", "+alpha+", "+beta+") is not a correct Fuzzy !");
		}
		this.a = a;
		this.b = b;
		this.alpha = alpha;
		this.beta   = beta;
	}
	
	/**
	 * Create a Fuzzy from a fuzzy number.
	 * 
	 * @param f - the fuzzy to copy
	 */
	public Fuzzy(Fuzzy f) {
		this.a = f.getA();
		this.b = f.getB();
		this.alpha = f.getAlpha();
		this.beta   = f.getBeta();
	}

	/**
	 * Get the fuzzy "?" (-inf,+inf,0,0)
	 * 
	 * @return - the fuzzy "?"
	 */
	public static Fuzzy whateverFuzzy(){
		try {
			return new Fuzzy(Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, 0, 0);
		} catch (InvalidFuzzyValueException e) {
			return null;
		}
	}
	
	/**
	 * Parse a human readable representation of a fuzzy number in order 
	 * to get a Fuzzy object.
	 * 
	 * @param s - the string to parse
	 * @return - the correspondent fuzzy number
	 * @throws FuzzyFormatException 
	 * @throws InvalidFuzzyValueException 
	 */
	public static Fuzzy parse(String s) throws FuzzyFormatException, InvalidFuzzyValueException {
		Fuzzy result = null;
		Pattern p1 = Pattern.compile("([<>]?)(~*)([-]?[0-9]+(\\.[0-9]*)?)");
		Matcher m1 = p1.matcher(s);
		Pattern p2 = Pattern.compile("(~*)([-]?[0-9]*(\\.[0-9]*)?)-(~*)([-]?[0-9]*(\\.[0-9]*)?)");
		Matcher m2 = p2.matcher(s);
		//1 : ? case
		if(s.equals("?")) {
				result = new Fuzzy(Float.NEGATIVE_INFINITY,Float.POSITIVE_INFINITY,0,0);
		}
		//2 : a "simple" number
		else if(m1.matches()) {
			// Group 1 : <, > or nothing
			// Group 2 : [~*]
			// Group 3 : int as a string
			int ntilde = m1.group(2).length();
			float n = Float.parseFloat(m1.group(3));
			float a = n*ntilde*INCREMENT/100;
			//without < or >
			if (m1.group(1).equals("")) {
				result = new Fuzzy(n,n,a,a);
			}
			//with < or >
			else {
				if(m1.group(1).equals("<")) {
					result = new Fuzzy(Float.NEGATIVE_INFINITY,n,0,a);
				}
				else {
					result = new Fuzzy(n,Float.POSITIVE_INFINITY,a,0);
				}
			}
		}
		//3 : an "interval"
		else if(m2.matches()){
			//Group 1 : first [~*]
			//Group 2 : n1
			//Group 4 : second [~*]
			//Group 5 : n2
			int n1tilde = m2.group(1).length();
			float n1 = Float.parseFloat(m2.group(2));
			int n2tilde = m2.group(4).length();
			float n2 = Float.parseFloat(m2.group(5));
			float c = n1*n1tilde*INCREMENT/100; //alpha
			float d = n2*n2tilde*INCREMENT/100; //beta
			result = new Fuzzy(n1,n2,c,d);
		}
		else {
			throw new FuzzyFormatException();
		}

		return result;

	}
	
	/**
	 * Format a Fuzzy object into a human readable representation 
	 * of a fuzzy number.
	 * 
	 * @return the formatted string of this Fuzzy object.
	 * @throws InvalidAttributeValueException
	 */
	public String format() throws InvalidAttributeValueException{
		StringBuffer sb = new StringBuffer();
		// a = -∞
		if(a == Float.NEGATIVE_INFINITY){
			// b = +∞
			if(b == Float.POSITIVE_INFINITY){
				sb.append("?");
			// b is a strict float number
			}else if(b != Float.NaN && b != Float.NEGATIVE_INFINITY){
				sb.append("<");
				appendFuzzyNumber(sb, b, beta);
			}else{
				throw new InvalidAttributeValueException();
			}
		// a != (-/+)∞
		}else if(a != Float.NaN && a != Float.POSITIVE_INFINITY){
			// b = +∞
			if(b == Float.POSITIVE_INFINITY){
				sb.append(">");
				appendFuzzyNumber(sb, a, alpha);
			// b is a strict float number
			}else if(b != Float.NaN && b != Float.NEGATIVE_INFINITY){
				// a = b
				if(Float.compare(a, b) == 0
						&& Float.compare(alpha, beta) == 0){
					appendFuzzyNumber(sb, a, alpha);
				}else{
					appendFuzzyNumber(sb, a, alpha);
					sb.append("-");
					appendFuzzyNumber(sb, b, beta);
				}
			}else{
				throw new InvalidAttributeValueException();
			}
		}else{
			throw new InvalidAttributeValueException();
		}
		return sb.toString();
	}
	
	/**
	 * Do a pattern matching between 2 fuzzies and get the likelihood.
	 * 
	 * @param p - a fuzzy number, the pattern
	 * @param d - a fuzzy number, the data
	 * @return the likelihood between p and d
	 */
	public static float getLikelihood(Fuzzy p,Fuzzy d) {
		float ap,bp,alphap,betap;
		float ad,bd,alphad,betad;
		float n1,n2;
		float likelihood;
		ap = p.getA();
		bp = p.getB();
		alphap = p.getAlpha();
		betap  = p.getBeta();
		ad = d.getA();
		bd = d.getB();
		alphad = d.getAlpha();
		betad  = d.getBeta();	
		//disjoint supports ?
		if((bp+betap<ad-alphad) || (bd+betad<ap-alphap)){
			likelihood = 0;
			logger.debug("disjoint supports: \""+p+"\"<->\""+d+"\"");
		}
		//intersecting kernels ?
		else if(isIn(ap, ad, bd) || isIn(bp, ad, bd) || isIn(ad,ap,bp) || isIn(bd,ap,bp)){
			likelihood = 1;
			logger.debug("intersecting kernels: \""+p+"\"<->\""+d+"\"");
		}
		//zero denominators ?
		else if(alphad+betap == 0) {
				n2 = max(0,1-(ap-bd)/(alphap+betad));
				likelihood = min(1,n2);
				logger.debug("zero denominators 1: \""+p+"\"<->\""+d+"\"");
		}
		else if(alphap+betad == 0) {
			n1 = max(0,1-(ad-bp)/(alphad+betap));
			likelihood = min(1,n1);
			logger.debug("zero denominators 2: \""+p+"\"<->\""+d+"\"");
		}
		//general case
		else {
			n1 = max(0,1-(ad-bp)/(alphad+betap));
			n2 = max(0,1-(ap-bd)/(alphap+betad));
			likelihood = min(1,n1,n2);
			logger.debug("general case: \""+p+"\"<->\""+d+"\"");
		}
		return likelihood;
	}
	
	/**
	 * Do a pattern matching between 2 fuzzies and get the necessity.
	 * 
	 * @param p - a fuzzy number, the pattern
	 * @param d -  a fuzzy number, the data
	 * @return the necessity between p and d
	 */
	public static float getNecessity(Fuzzy d,Fuzzy p) {
		float ap,bp,alphap,betap;
		float ad,bd,alphad,betad;
		float n1,n2;
		float necessity;
		ap = p.getA();
		bp = p.getB();
		alphap = p.getAlpha();
		betap  = p.getBeta();
		ad = d.getA();
		bd = d.getB();
		alphad = d.getAlpha();
		betad  = d.getBeta();	
		String s = "";
		//data kernel out of pattern support ?
		if((bp+betap<bd) || (ad<ap-alphap)) {
			necessity = 0;
			s="data kernel out of pattern support";
		}
		// Data support in pattern kernel ?
		else if(isIn(ad-alphad, ap, bp) && isIn(bd+betad, ap, bp)) {
			necessity = 1;
			s="Data support in pattern kernel ";
		}
		//zero denominators ?
		else if(betap+betad == 0) {
			n2 = max(0,(ad-ap+alphap)/(alphap+alphad));
			necessity = min(1,n2);
			s="betap+betad == 0";
		}
		else if(alphap+alphad == 0) {
			n1 = max(0,(bp-bd+betap)/(betap+betad));
			necessity = min(1,n1);
			s="alphap+alphad == 0";
		}
		//General case
		else {
			n1 = max(0,(bp-bd+betap)/(betap+betad));
			n2 = max(0,(ad-ap+alphap)/(alphap+alphad));
			necessity = min(1,n1,n2);
			s="General case : "+"n1="+n1+", n2="+n2;
		}
		if(Float.compare(necessity, Float.NaN) == 0) System.err.println("*************\n"+s+"\nd = "+d.explicitToString()+"\np =  "+p.explicitToString()+"\nn = "+necessity);
		return necessity;
	}

	/**
	 * To know whether a Fuzzy has a lower bound or not
	 */
	public boolean hasAFuzzyLowerBound(){
		return Float.compare(alpha, 0) != 0;
	}

	/**
	 * To know whether a Fuzzy has a upper bound or not 
	 */
	public boolean hasAFuzzyUpperBound(){
		return Float.compare(beta, 0) != 0;
	}

	/**
	 * @return [gamma/(c*INCREMENT)*100]
 	 */
	private int getTildeNb(float c, float gamma){
		return Math.abs(Math.round(((gamma/c)*100)/INCREMENT));
	}

	private void appendFuzzyNumber(StringBuffer sb, float c, float gamma){
		for(int i=0; i<getTildeNb(c, gamma); i++){
			sb.append("~");
		}
		sb.append(c);
	}

	public float getA() {
		return a;
	}

	public float getB() {
		return b;
	}

	public float getAlpha() {
		return alpha;
	}

	public float getBeta() {
		return beta;
	}
	
	/**
	 * Get the min among the 3 parameters.
	 */
	private static float min(float a,float b, float c) {
		return Math.min(Math.min(a, b),c);
	}
	
	/**
	 * Get the max among the 2 parameters.
	 */
	private static float max(float a, float b) {
		return Math.max(a, b);
	}
	
	/**
	 * Get the min among the 2 parameters.
	 */
	private static float min(float a, float b) {
		return Math.min(a, b);
	}
	
	/**
	 * True if a <= x <= b
	 */
	private static boolean isIn(float x,float a, float b) {
		return ((a<=x) && (x<=b));
	}

	@Override
	public boolean equals(Object o){
		if(o instanceof Fuzzy){
			Fuzzy anotherFuzzy = (Fuzzy)o;
			return 	Float.compare(a ,anotherFuzzy.getA()) == 0
						&& Float.compare(b ,anotherFuzzy.getB()) == 0
						&& Float.compare(alpha ,anotherFuzzy.getAlpha()) == 0
						&& Float.compare(beta ,anotherFuzzy.getBeta()) == 0;
		}
		return false;
	}

	public String explicitToString(){
		return "( "+a+", "+b+", "+alpha+", "+beta+" )";
	}
	
	@Override
	public String toString(){
		String s = null;
		try {
			s = format();
		} catch (InvalidAttributeValueException e) {
			e.printStackTrace();
		}
		return s;
	}
}
